最近有一个项目需要设置开机自启动,没有预先为程序设定这个功能,所以部署的时候每次都要生成快捷方式,然后找自启动文件夹,搞得非常崩溃。所以决定填上这个坑,写了一段unity应用通用的开启和关闭开机自启的代码。

写在前面

经过一晚上的研究和分析,发现设置开机自启动主要有两种主流方式和一种非主流方式,这几个方式基本能满足需求,分别是:

  1. 开始菜单启动(最常用,不需要管理员权限)
  2. 注册表启动项(需要管理员权限)
  3. Windows计划任务(需要管理员权限,unity中使用有异常)

以下代码都是即拿即用,只需绑定Button和Text即可。

开始菜单启动

开始菜单启动大概是我们最常用的一种设置开机自启的方法,具体用程序来实现也是很简单的,主要有两步:

  1. 创建快捷方式并关联程序
  2. 将快捷方式存到“开始”菜单的“启动”目录

代码如下:


using System;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
using IWshRuntimeLibrary;

public class StartMenu : MonoBehaviour {

    public Button setupStartupButton;
    public Button cancelStartupButton;
    public Text hintText;
    private static string ShortcutName = "test.lnk";

    private void OnEnable()
    {
        isStartup();
        setupStartupButton.onClick.AddListener(OnSetupStartupButtonClick);
        cancelStartupButton.onClick.AddListener(OnCancelStartupButtonClick);
    }

    private void OnDisable()
    {
        setupStartupButton.onClick.RemoveListener(OnSetupStartupButtonClick);
        cancelStartupButton.onClick.RemoveListener(OnCancelStartupButtonClick);
    }

    private void OnSetupStartupButtonClick()
    {
        CreateShortcut(Environment.GetFolderPath(Environment.SpecialFolder.Startup), ShortcutName, System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
        isStartup();
    }

    private void OnCancelStartupButtonClick()
    {
        if (System.IO.File.Exists(Environment.GetFolderPath(Environment.SpecialFolder.Startup) + "\\" + ShortcutName))
            System.IO.File.Delete(Environment.GetFolderPath(Environment.SpecialFolder.Startup) + "\\" + ShortcutName);
        isStartup();
    }

    private void isStartup()
    {
        if (System.IO.File.Exists(Environment.GetFolderPath(Environment.SpecialFolder.Startup) + "\\" + ShortcutName))
            hintText.text = "应用已开机自启";
        else
            hintText.text = "应用非开机自启";
    }

    public static bool CreateShortcut(string direstory,string shortcurName,string targetPath,string description = null,string iconLocation = null)
    {

        try
        {
            if(!Directory.Exists(direstory))
            {
                Directory.CreateDirectory(direstory);
            }

            // 添加引用com中搜索Windows Script Host Object Model, 如果在unity中使用则需下载 Interop.IWshRuntimeLibrary.dll 并放到代码同一文件夹
            string shortscurPath = Path.Combine(direstory, string.Format("{0}", shortcurName));
            IWshRuntimeLibrary.WshShell shell = new IWshRuntimeLibrary.WshShell();
            IWshRuntimeLibrary.IWshShortcut shortcut = (IWshRuntimeLibrary.IWshShortcut)shell.CreateShortcut(shortscurPath); // 创建快捷方式对象
            shortcut.TargetPath = targetPath; // 指定目标路径
            shortcut.WorkingDirectory = Path.GetDirectoryName(targetPath); //设置起始位置
            shortcut.WindowStyle = 1; // 设置运行方式,默认为常规窗口
            shortcut.Description = description; // 设置备注
            shortcut.IconLocation = string.IsNullOrEmpty(iconLocation) ? targetPath : iconLocation; //设置图标路径
            shortcut.Save(); // 保存快捷方式
            return true;
        }
        catch
        {

        }

        return false;
    }
}

使用开始菜单自启是非常稳的一种方法,不需要有管理员权限,但是unity中使用需要添加引用,并下载 Interop.IWshRuntimeLibrary.dll 程序集放到与本代码同一文件夹。

注册表开机启动项

这个相信是大部分同学使用的情况,简单易懂隐蔽(只是感觉很隐蔽,但是 msconfig 立马暴露),代码很简单,将启动的项目名称、文件位置添加到启动项即可。

using Microsoft.Win32;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System;

public class Regeditkey : MonoBehaviour {

    public Button setupStartupButton;
    public Button cancelStartupButton;
    public Text hintText;

    private void OnEnable()
    {
        string path = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
        //regeditkey();
        setupStartupButton.onClick.AddListener(OnSetupStartupButtonClick);
        cancelStartupButton.onClick.AddListener(OnCancelStartupButtonClick);
    }

    private void OnDisable()
    {
        setupStartupButton.onClick.RemoveListener(OnSetupStartupButtonClick);
        cancelStartupButton.onClick.RemoveListener(OnCancelStartupButtonClick);
    }

    private void OnSetupStartupButtonClick()
    {
        // 提示,需要更改注册表
        try
        {
            string path = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
            RegistryKey rgkRun = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
            if(rgkRun == null)
            {
                rgkRun = Registry.LocalMachine.CreateSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run");
            }
            rgkRun.SetValue("dhstest", path); // 名字请自行设置
        }
        catch
        {
            Debug.Log(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
        }
        finally
        {
            regeditkey();
        }
    }

    private void OnCancelStartupButtonClick()
    {
        // 提示,需要更改注册表
        try
        {
            string path = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
            RegistryKey rgkRun = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
            if (rgkRun == null)
            {
                rgkRun = Registry.LocalMachine.CreateSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run");
            }
            rgkRun.DeleteValue("dhstest", false);
        }
        catch
        {
            Debug.Log("error");
        }
        finally
        {
            regeditkey();
        }
    }


    private void regeditkey()
    {
        RegistryKey rgkRun = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run",true);
        if(rgkRun.GetValue("dhstest") == null)
        {
            hintText.text = "自启动为关闭";
        }
        else
        {
            hintText.text = "自启动为打开";
        }
    }
}

此方法在unity中使用也稳定有效,不需要下载程序集,但是需要以管理员权限运行程序才能修改注册表。

Windows 计划任务方式启动

计划任务表有很多人都不熟悉,但是它确实有很多妙用。但是在把代码转成unity应用后发现unity开发的应用中无法使用,初步认定的原因是,Interop.TaskScheduler.dll 程序集需要设置嵌入互操作类型为 false 但是unity 开发的应用中应用的程序集并没有这个选项,所以用 unity 大概不能使用Windows计划任务管理器。

为了验证计划任务管理器是否有效,我在wpf应用上测试了一下,wpf的引用是有设置嵌入互操作类型选项的,将其改为false即可正常引用 Interop.TaskScheduler 程序集。

image

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using TaskScheduler;

namespace Task
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        private string TaskName = "test";

        public MainWindow()
        {
            InitializeComponent();
            isStartup();
        }
        
        private void OnSetupStartupButtonClick(object sender, RoutedEventArgs e)
        {
            if (!HaveTaskScheduler(TaskName))
                CreateTaskScheduler(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName, "xtw", "Set startup");
            else
                GetTaskScheduler(TaskName).Enabled = true;
            isStartup();
        }

        private void OnCancelStartupButtonClick(object sender, RoutedEventArgs e)
        {
            if (HaveTaskScheduler(TaskName))
                GetTaskScheduler(TaskName).Enabled = false;
            isStartup();
        }

        private bool HaveTaskScheduler(string name)
        {
            IRegisteredTask task = GetTaskScheduler(name);
            if (task == null)
                return false;
            else
                return true;
        }

        private void isStartup()
        {
            if (HaveTaskScheduler(TaskName) && GetTaskScheduler(TaskName).Enabled)
                text.Content = "应用已开机自启"; // 此text是Windows 组件Label
            else
                text.Content = "应用非开机自启";
        }

        private static IRegisteredTask GetTaskScheduler(string name)
        {
            // 新建任务
            TaskSchedulerClass scheduler = new TaskSchedulerClass();
            // 连接
            scheduler.Connect(null, null, null, null);
            // 获取创建任务的目录
            ITaskFolder folder = scheduler.GetFolder("\\");
            IRegisteredTask task;
            try
            {
                task = folder.GetTask(name);
            }
            catch
            {
                return null;
            }
            return task;
        }

        private void CreateTaskScheduler(string file, string author, string desc)
        {
            // 新建任务
            TaskSchedulerClass scheduler = new TaskSchedulerClass();
            // 连接
            scheduler.Connect(null, null, null, null);
            // 获取创建任务的目录
            ITaskFolder folder = scheduler.GetFolder("\\");
            // 设置参数
            ITaskDefinition task = scheduler.NewTask(0);
            task.RegistrationInfo.Author = author; // 创建者
            task.RegistrationInfo.Description = desc; //描述
                                                      // 设置触发机制 (此处是 登录后)
            task.Triggers.Create(_TASK_TRIGGER_TYPE2.TASK_TRIGGER_LOGON);
            // 设置动作 (此处为 exe运行程序)
            IExecAction action = (IExecAction)task.Actions.Create(_TASK_ACTION_TYPE.TASK_ACTION_EXEC);
            action.Path = file; //设置文件目录
            //task.Settings.ExecutionTimeLimit = "PTOS"; //运行任务时间超时停止任务吗?PTOS不开启超时
            task.Settings.DisallowStartIfOnBatteries = false; //只在交流电下才执行
            task.Settings.RunOnlyIfIdle = false; // 仅当计算机空闲下才执行

            IRegisteredTask regTask =
                folder.RegisterTaskDefinition(TaskName, task, // 此处需要设置任务的名称(name)
                (int)_TASK_CREATION.TASK_CREATE, null, // user
                null,//passward
                _TASK_LOGON_TYPE.TASK_LOGON_INTERACTIVE_TOKEN,
                "");
            IRunningTask runTask = regTask.Run(null);
        }
    }
}

总结

对于开发一般应用来说,开始菜单启动是最简单、有效的方法。对于开发 unity 应用来说,这也不失为是一种好方法。

参考链接